package fs

import (
	"crypto/md5"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"github.com/satori/go.uuid"
	"hash/crc64"
	"io"
	"io/ioutil"
	"moss/conf"
	"moss/status"
	"os"
	"path"
	"strconv"
	"time"
	"context"
	"log"
)

func GetObjectLocation(bucketName, objectName string, version int) string {
	objectSHA := sha256.Sum256([]byte(objectName))
	bucketSHA := sha256.Sum256([]byte(bucketName))
	objectHashed := hex.EncodeToString(objectSHA[:])
	bucketHashed := hex.EncodeToString(bucketSHA[:])
	return path.Join(conf.Config.Path.Data, bucketHashed[:4], bucketHashed, objectHashed[:4], objectHashed)
}

func GetBucketLocation(bucketName string) string {
	bucketSHA := sha256.Sum256([]byte(bucketName))
	bucketHashed := hex.EncodeToString(bucketSHA[:])
	return path.Join(conf.Config.Path.Data, bucketHashed[:4], bucketHashed)
}

func SafeOpenFile(filename string, size int64) *os.File {
	fileInfo, e := os.Stat(filename)
	if e != nil {
		return nil
	}
	if fileInfo.Size() != size {
		return nil
	}
	f, e := os.Open(filename)
	if e != nil {
		return nil
	}
	return f
}

func RemoveFile(filename string) {
	os.Remove(filename)
}

func min(a int64, b int64) int64 {
	if a < b {
		return a
	} else {
		return b
	}
}

func MD5Hex2Base64(hexStr string) string {
	sum, err := hex.DecodeString(hexStr)
	if err != nil || len(sum) != 16 {
		return ""
	}
	base64hash := base64.StdEncoding.EncodeToString(sum)
	return base64hash
}

func CalculateMD5(filename, encryption string) string {
	fileHandle, err := os.OpenFile(filename, os.O_RDONLY, 0644)
	if err != nil {
		log.Println(err)
		return ""
	}
	defer fileHandle.Close()

	var src io.Reader = fileHandle
	if encryption == "AES256" {
		privateKey, _ := hex.DecodeString(conf.Config.Security.AesPrivateKey)
		src = EncryptReader(fileHandle, privateKey)
	}

	hasher := md5.New()
	_, err = io.Copy(hasher, src)
	if err != nil {
		log.Println(err)
		return ""
	}

	return hex.EncodeToString(hasher.Sum(nil))
}

func CalculateMD5Async(filename, encryption string) <-chan string {
	chanComplete := make(chan string, 1)
	go func() {
		chanComplete <- CalculateMD5(filename, encryption)
	}()
	return chanComplete
}

func GenerateSymlinkUUID(bucket string, object string, privateKey string) string {
	identifier, err := uuid.FromString(bucket + "/" + object)
	if err != nil {
		return ""
	}

	return uuid.NewV5(identifier, privateKey).String()
}

func GenerateContentUUID(filePath, encryption, privateKey string) string {
	checksum := CalculateMD5(filePath, encryption)
	if len(checksum) == 0 {
		return ""
	}

	byteBuf, err := hex.DecodeString(checksum)
	if err != nil {
		return ""
	}

	identifier, err := uuid.FromBytes(byteBuf)
	if err != nil {
		return ""
	}

	return uuid.NewV5(identifier, privateKey).String()
}

func CalculateCRC64(filename string) string {
	fileHandle, err := os.Open(filename)
	if err != nil {
		return ""
	}

	defer fileHandle.Close()

	crcTable := crc64.MakeTable(crc64.ECMA)
	hasher := crc64.New(crcTable)
	_, err = io.Copy(hasher, fileHandle)
	if err != nil {
		return ""
	}

	return strconv.FormatUint(hasher.Sum64(), 10)
}

func CalculateCRC64Async(filename string) <-chan string {
	chanComplete := make(chan string, 1)
	go func() {
		chanComplete <- CalculateCRC64(filename)
	}()
	return chanComplete
}

type readerFunc func(p []byte) (n int, err error)
func (rf readerFunc) Read(p []byte) (n int, err error) { return rf(p) }

func deadlineCopy(ctx context.Context, dst io.Writer, src io.Reader) error {
	_, err := io.Copy(dst, readerFunc(func(p []byte) (int, error) {
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		default:
			return src.Read(p)
		}
	}))

	return err
}

func doObjectCopy(dst io.Writer, src io.Reader, size int64, option map[string]string) status.Status {
	if size < 0 {
		return status.InvalidArgument
	}

	if size == 0 {
		return status.Success
	}

	privateKey, _ := hex.DecodeString(conf.Config.Security.AesPrivateKey)
	var wrappedDst = dst
	var wrappedSrc = src
	if option["src-encrypt"] == "AES256" {
		wrappedSrc = EncryptReader(src, privateKey)
	}
	if option["dst-encrypt"] == "AES256" {
		wrappedDst = EncryptWriter(dst, privateKey)
	}

	timeout := time.Second * time.Duration(conf.Config.Limit.MaxTransferSeconds)
	ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(timeout))
	err := deadlineCopy(ctx, wrappedDst, io.LimitReader(wrappedSrc, size))
	if err == nil {
		return status.Success
	}

	// Internal error here
	log.Println(err)
	if err == context.DeadlineExceeded {
		return status.RequestTimeout
	} else {
		return status.InternalError
	}

	/*
	statusChan := make(chan status.Status, 1)
	stopChan := make(chan bool, 1)

	go func() {
		privateKey, _ := hex.DecodeString(conf.Config.Security.AesPrivateKey)
		var wrappedDst = dst
		var wrappedSrc = src
		if option["src-encrypt"] == "AES256" {
			wrappedSrc = EncryptReader(src, privateKey)
		}
		if option["dst-encrypt"] == "AES256" {
			wrappedDst = EncryptWriter(dst, privateKey)
		}

		var written int64 = 0
		for written < size {
			select {
			case <-stopChan:
				statusChan <- status.RequestTimeout
				return
			default:
				shouldCopy := min(conf.Config.Limit.FileTransferBufferSize, size-written)
				nbytes, err := io.CopyN(wrappedDst, wrappedSrc, shouldCopy)
				if err != nil {
					statusChan <- status.InternalError
					return
				}
				written += nbytes
			}
		}
		statusChan <- status.Success
	}()

	timeout := time.Second * time.Duration(conf.Config.Limit.MaxTransferSeconds)
	select {
	case <-time.After(timeout):
		stopChan <- true
		return status.RequestTimeout
	case rc := <-statusChan:
		return rc
	}
	*/
}

func prepareDirectory(dir string) error {
	_, err := os.Stat(dir)
	if os.IsNotExist(err) {
		err = os.MkdirAll(dir, 0755)
	}
	return err
}

func prepareTempFile() (tempFile *os.File, fileName string, err error) {
	tempDir := conf.Config.Path.Temp

	err = prepareDirectory(tempDir)
	if err != nil {
		return nil, "", err
	}

	tempFile, err = ioutil.TempFile(tempDir, "tmp-")
	if err != nil {
		return nil, "", err
	}

	return tempFile, tempFile.Name(), nil
}

func WriteFile(filename string, document io.Reader, size int64, encryption string) status.Status {
	if err := prepareDirectory(path.Dir(filename)); err != nil {
		log.Println(err)
		return status.InternalError
	}

	tempFile, tempFileName, err := prepareTempFile()
	if err != nil {
		log.Println(err)
		return status.InternalError
	}

	defer tempFile.Close()

	rc := doObjectCopy(tempFile, document, size, map[string]string{"dst-encrypt": encryption})

	if rc != status.Success {
		os.Remove(tempFileName)
		return rc
	}

	err = os.Rename(tempFileName, filename)
	if err != nil {
		log.Println(err)
		os.Remove(tempFileName)
		return status.InternalError
	}

	return status.Success
}

func AppendFile(filename string, document io.Reader, offset, size int64, encryption string) status.Status {
	fileHandle, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return status.InternalError
	}
	defer fileHandle.Close()

	_, err = fileHandle.Seek(offset, io.SeekStart)
	if err != nil {
		return status.InternalError
	}

	rc := doObjectCopy(fileHandle, document, size, map[string]string{"dst-encrypt": encryption})

	// If doObjectCopy failed, the extra area is garage, and we can simply ignore them
	return rc
}

func CopyFile(targetFilename, sourceFilename, targetEncryptMode, sourceEncryptMode string, size int64) status.Status {
	if prepareDirectory(path.Dir(targetFilename)) != nil {
		return status.InternalError
	}

	tempFileHandle, tempFileName, err := prepareTempFile()
	if err != nil {
		return status.InternalError
	}

	sourceFileHandle, err := os.OpenFile(sourceFilename, os.O_RDONLY, 0644)
	if err != nil {
		return status.InternalError
	}
	defer sourceFileHandle.Close()

	rc := doObjectCopy(tempFileHandle, sourceFileHandle, size,
		map[string]string{"dst-encrypt": targetEncryptMode, "src-encrypt": sourceEncryptMode})

	if rc != status.Success {
		tempFileHandle.Close()
		os.Remove(tempFileName)
		return rc
	}

	tempFileHandle.Close()
	err = os.Rename(tempFileName, targetFilename)
	if err != nil {
		os.Remove(tempFileName)
		return status.InternalError
	}

	return status.Success
}
